diff --git a/HISTORY.md b/HISTORY.md
index 88404b3..0a8ed82 100644
--- a/HISTORY.md
+++ b/HISTORY.md
@@ -1,6 +1,12 @@
Release History
===============
+0.1.6 (2026-02-16)
+------------------
+
+- Add support for request timeouts.
+- Add status and error codes support - https://serpapi.com/api-status-and-error-codes
+
0.1.5 (2023-11-01)
------------------
diff --git a/README.md b/README.md
index b2bc441..1510dd9 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@
SerpApi Python Library & Package
- 
+ 
[](https://github.com/serpapi/serpapi-python/actions/workflows/ci.yml)
@@ -52,12 +52,13 @@ The [SerpApi.com API Documentation](https://serpapi.com/search-api) contains a l
### Error handling
-Unsuccessful requests raise `serpapi.HTTPError` exception. The returned status code will reflect the sort of error that occurred, please refer to [Status and Error Codes Documentation](https://serpapi.com/api-status-and-error-codes) for more details.
+Unsuccessful requests raise `serpapi.HTTPError` or `serpapi.TimeoutError` exceptions. The returned status code will reflect the sort of error that occurred, please refer to [Status and Error Codes Documentation](https://serpapi.com/api-status-and-error-codes) for more details.
```python
import serpapi
-client = serpapi.Client(api_key=os.getenv("API_KEY"))
+# A default timeout can be set here.
+client = serpapi.Client(api_key=os.getenv("API_KEY"), timeout=10)
try:
results = client.search({
@@ -71,12 +72,17 @@ except serpapi.HTTPError as e:
pass
elif e.status_code == 429: # Exceeds the hourly throughput limit OR account run out of searches
pass
+except serpapi.TimeoutError as e:
+ # Handle timeout
+ print(f"The request timed out: {e}")
```
## Documentation
Documentation is [available on Read the Docs](https://serpapi-python.readthedocs.io/en/latest/).
+Change history is [available on GitHub](https://github.com/serpapi/serpapi-python/blob/master/HISTORY.md).
+
## Basic Examples in Python
### Search Bing
diff --git a/README.md.erb b/README.md.erb
deleted file mode 100644
index 0bed824..0000000
--- a/README.md.erb
+++ /dev/null
@@ -1,165 +0,0 @@
-<%-
-def snippet(format, path)
- lines = File.new(path).readlines
- stop = lines.size - 1
- slice = lines[7..stop]
- slice.reject! { |l| l.match?(/(^# |assert )/) }
- buf = slice.map { |l| l.gsub(/(^\s{2})/, '').gsub(/^\s*$/, '') }.join
- url = 'https://github.com/serpapi/serpapi-python/blob/master/' + path
- %Q(```#{format}\nimport serpapi\nimport pprint\nimport os\n\n#{buf}```\ntest: [#{path}](#{url}))
-end
--%>
-
-
-
SerpApi Python Library & Package
-

-
-
- [](https://github.com/serpapi/serpapi-python/actions/workflows/ci.yml)
-
-
-This repository is the home of the *soon–to–be* official Python API wrapper for [SerpApi](https://serpapi.com). This `serpapi` module allows you to access search data in your Python application.
-
-[SerpApi](https://serpapi.com) supports Google, Google Maps, Google Shopping, Bing, Baidu, Yandex, Yahoo, eBay, App Stores, and more. Check out the [documentation](https://serpapi.com/search-api) for a full list.
-
-## Current Status
-
-This project is under development, and will be released to the public on PyPi soon.
-
-## Installation
-
-To install the `serpapi` package, simply run the following command:
-
-```bash
-$ pip install serpapi
-```
-
-Please note that this package is separate from the *soon–to–be* legacy `serpapi` module, which is currently available on PyPi as `google-search-results`.
-
-## Usage
-
-Let's start by searching for Coffee on Google:
-
-```pycon
->>> import serpapi
->>> s = serpapi.search(q="Coffee", engine="google", location="Austin, Texas", hl="en", gl="us")
-```
-
-The `s` variable now contains a `SerpResults` object, which acts just like a standard dictionary, with some convenient functions added on top.
-
-Let's print the first result:
-
-```pycon
->>> s["organic_results"][0]["link"]
-'https://en.wikipedia.org/wiki/Coffee'
-```
-
-Let's print the title of the first result, but in a more Pythonic way:
-
-```pycon
->>> s["organic_results"][0].get("title")
-'Coffee - Wikipedia'
-```
-
-The [SerpApi.com API Documentation](https://serpapi.com/search-api) contains a list of all the possible parameters that can be passed to the API.
-
-## Documentation
-
-Documentation is [available on Read the Docs](https://serpapi-python.readthedocs.io/en/latest/).
-
-## Examples in python
-Here is how to calls the APIs.
-
-### Search bing
-<%= snippet('python', 'tests/example_search_bing_test.py') %>
-see: [serpapi.com/bing-search-api](https://serpapi.com/bing-search-api)
-
-### Search baidu
-<%= snippet('python', 'tests/example_search_baidu_test.py') %>
-see: [serpapi.com/baidu-search-api](https://serpapi.com/baidu-search-api)
-
-### Search yahoo
-<%= snippet('python', 'tests/example_search_yahoo_test.py') %>
-see: [serpapi.com/yahoo-search-api](https://serpapi.com/yahoo-search-api)
-
-### Search youtube
-<%= snippet('python', 'tests/example_search_youtube_test.py') %>
-see: [serpapi.com/youtube-search-api](https://serpapi.com/youtube-search-api)
-
-### Search walmart
-<%= snippet('python', 'tests/example_search_walmart_test.py') %>
-see: [serpapi.com/walmart-search-api](https://serpapi.com/walmart-search-api)
-
-### Search ebay
-<%= snippet('python', 'tests/example_search_ebay_test.py') %>
-see: [serpapi.com/ebay-search-api](https://serpapi.com/ebay-search-api)
-
-### Search naver
-<%= snippet('python', 'tests/example_search_naver_test.py') %>
-see: [serpapi.com/naver-search-api](https://serpapi.com/naver-search-api)
-
-### Search home depot
-<%= snippet('python', 'tests/example_search_home_depot_test.py') %>
-see: [serpapi.com/home-depot-search-api](https://serpapi.com/home-depot-search-api)
-
-### Search apple app store
-<%= snippet('python', 'tests/example_search_apple_app_store_test.py') %>
-see: [serpapi.com/apple-app-store](https://serpapi.com/apple-app-store)
-
-### Search duckduckgo
-<%= snippet('python', 'tests/example_search_duckduckgo_test.py') %>
-see: [serpapi.com/duckduckgo-search-api](https://serpapi.com/duckduckgo-search-api)
-
-### Search google
-<%= snippet('python', 'tests/example_search_google_test.py') %>
-see: [serpapi.com/search-api](https://serpapi.com/search-api)
-
-### Search google scholar
-<%= snippet('python', 'tests/example_search_google_scholar_test.py') %>
-see: [serpapi.com/google-scholar-api](https://serpapi.com/google-scholar-api)
-
-### Search google autocomplete
-<%= snippet('python', 'tests/example_search_google_autocomplete_test.py') %>
-see: [serpapi.com/google-autocomplete-api](https://serpapi.com/google-autocomplete-api)
-
-### Search google product
-<%= snippet('python', 'tests/example_search_google_product_test.py') %>
-see: [serpapi.com/google-product-api](https://serpapi.com/google-product-api)
-
-### Search google reverse image
-<%= snippet('python', 'tests/example_search_google_reverse_image_test.py') %>
-see: [serpapi.com/google-reverse-image](https://serpapi.com/google-reverse-image)
-
-### Search google events
-<%= snippet('python', 'tests/example_search_google_events_test.py') %>
-see: [serpapi.com/google-events-api](https://serpapi.com/google-events-api)
-
-### Search google local services
-<%= snippet('python', 'tests/example_search_google_local_services_test.py') %>
-see: [serpapi.com/google-local-services-api](https://serpapi.com/google-local-services-api)
-
-### Search google maps
-<%= snippet('python', 'tests/example_search_google_maps_test.py') %>
-see: [serpapi.com/google-maps-api](https://serpapi.com/google-maps-api)
-
-### Search google jobs
-<%= snippet('python', 'tests/example_search_google_jobs_test.py') %>
-see: [serpapi.com/google-jobs-api](https://serpapi.com/google-jobs-api)
-
-### Search google play
-<%= snippet('python', 'tests/example_search_google_play_test.py') %>
-see: [serpapi.com/google-play-api](https://serpapi.com/google-play-api)
-
-### Search google images
-<%= snippet('python', 'tests/example_search_google_images_test.py') %>
-see: [serpapi.com/images-results](https://serpapi.com/images-results)
-
-
-## License
-
-MIT License.
-
-## Contributing
-
-Bug reports and pull requests are welcome on GitHub. Once dependencies are installed, you can run the tests with `pytest`.
diff --git a/serpapi/__version__.py b/serpapi/__version__.py
index 1276d02..0a8da88 100644
--- a/serpapi/__version__.py
+++ b/serpapi/__version__.py
@@ -1 +1 @@
-__version__ = "0.1.5"
+__version__ = "0.1.6"
diff --git a/serpapi/core.py b/serpapi/core.py
index a627d21..7d4454a 100644
--- a/serpapi/core.py
+++ b/serpapi/core.py
@@ -25,6 +25,9 @@ class Client(HTTPClient):
DASHBOARD_URL = "https://serpapi.com/dashboard"
+ def __init__(self, *, api_key=None, timeout=None):
+ super().__init__(api_key=api_key, timeout=timeout)
+
def __repr__(self):
return ""
@@ -60,10 +63,16 @@ def search(self, params: dict = None, **kwargs):
if params is None:
params = {}
+ # These are arguments that should be passed to the underlying requests.request call.
+ request_kwargs = {}
+ for key in ["timeout", "proxies", "verify", "stream", "cert"]:
+ if key in kwargs:
+ request_kwargs[key] = kwargs.pop(key)
+
if kwargs:
params.update(kwargs)
- r = self.request("GET", "/search", params=params)
+ r = self.request("GET", "/search", params=params, **request_kwargs)
return SerpResults.from_http_response(r, client=self)
@@ -80,6 +89,12 @@ def search_archive(self, params: dict = None, **kwargs):
if params is None:
params = {}
+ # These are arguments that should be passed to the underlying requests.request call.
+ request_kwargs = {}
+ for key in ["timeout", "proxies", "verify", "stream", "cert"]:
+ if key in kwargs:
+ request_kwargs[key] = kwargs.pop(key)
+
if kwargs:
params.update(kwargs)
@@ -90,7 +105,7 @@ def search_archive(self, params: dict = None, **kwargs):
f"Please provide 'search_id', found here: { self.DASHBOARD_URL }"
)
- r = self.request("GET", f"/searches/{ search_id }", params=params)
+ r = self.request("GET", f"/searches/{ search_id }", params=params, **request_kwargs)
return SerpResults.from_http_response(r, client=self)
def locations(self, params: dict = None, **kwargs):
@@ -106,6 +121,12 @@ def locations(self, params: dict = None, **kwargs):
if params is None:
params = {}
+ # These are arguments that should be passed to the underlying requests.request call.
+ request_kwargs = {}
+ for key in ["timeout", "proxies", "verify", "stream", "cert"]:
+ if key in kwargs:
+ request_kwargs[key] = kwargs.pop(key)
+
if kwargs:
params.update(kwargs)
@@ -114,6 +135,7 @@ def locations(self, params: dict = None, **kwargs):
"/locations.json",
params=params,
assert_200=True,
+ **request_kwargs,
)
return r.json()
@@ -129,10 +151,16 @@ def account(self, params: dict = None, **kwargs):
if params is None:
params = {}
+ # These are arguments that should be passed to the underlying requests.request call.
+ request_kwargs = {}
+ for key in ["timeout", "proxies", "verify", "stream", "cert"]:
+ if key in kwargs:
+ request_kwargs[key] = kwargs.pop(key)
+
if kwargs:
params.update(kwargs)
- r = self.request("GET", "/account.json", params=params, assert_200=True)
+ r = self.request("GET", "/account.json", params=params, assert_200=True, **request_kwargs)
return r.json()
diff --git a/serpapi/exceptions.py b/serpapi/exceptions.py
index 423cb91..c268c4d 100644
--- a/serpapi/exceptions.py
+++ b/serpapi/exceptions.py
@@ -43,3 +43,9 @@ class HTTPConnectionError(HTTPError, requests.exceptions.ConnectionError, SerpAp
"""Connection Error."""
pass
+
+
+class TimeoutError(requests.exceptions.Timeout, SerpApiError):
+ """Timeout Error."""
+
+ pass
diff --git a/serpapi/http.py b/serpapi/http.py
index c4f6ed1..16da0da 100644
--- a/serpapi/http.py
+++ b/serpapi/http.py
@@ -3,6 +3,7 @@
from .exceptions import (
HTTPError,
HTTPConnectionError,
+ TimeoutError,
)
from .__version__ import __version__
@@ -13,10 +14,11 @@ class HTTPClient:
BASE_DOMAIN = "https://serpapi.com"
USER_AGENT = f"serpapi-python, v{__version__}"
- def __init__(self, *, api_key=None):
+ def __init__(self, *, api_key=None, timeout=None):
# Used to authenticate requests.
# TODO: do we want to support the environment variable? Seems like a security risk.
self.api_key = api_key
+ self.timeout = timeout
self.session = requests.Session()
def request(self, method, path, params, *, assert_200=True, **kwargs):
@@ -34,12 +36,18 @@ def request(self, method, path, params, *, assert_200=True, **kwargs):
try:
headers = {"User-Agent": self.USER_AGENT}
+ # Use the default timeout if one was provided to the client.
+ if self.timeout and "timeout" not in kwargs:
+ kwargs["timeout"] = self.timeout
+
r = self.session.request(
method=method, url=url, params=params, headers=headers, **kwargs
)
except requests.exceptions.ConnectionError as e:
raise HTTPConnectionError(e)
+ except requests.exceptions.Timeout as e:
+ raise TimeoutError(e)
# Raise an exception if the status code is not 200.
if assert_200:
diff --git a/tests/test_timeout.py b/tests/test_timeout.py
new file mode 100644
index 0000000..7ce5fbd
--- /dev/null
+++ b/tests/test_timeout.py
@@ -0,0 +1,39 @@
+import pytest
+import requests
+from serpapi import Client
+
+def test_client_timeout_setting():
+ """Test that timeout can be set on the client and is passed to the request."""
+ client = Client(api_key="test_key", timeout=10)
+ assert client.timeout == 10
+
+def test_request_timeout_override(monkeypatch):
+ """Test that timeout can be overridden in the search method."""
+ client = Client(api_key="test_key", timeout=10)
+
+ def mock_request(method, url, params, headers, timeout, **kwargs):
+ assert timeout == 5
+ # Return a mock response object
+ mock_response = requests.Response()
+ mock_response.status_code = 200
+ mock_response._content = b'{"search_metadata": {"id": "123"}}'
+ return mock_response
+
+ monkeypatch.setattr(client.session, "request", mock_request)
+
+ client.search(q="coffee", timeout=5)
+
+def test_request_default_timeout(monkeypatch):
+ """Test that the client's default timeout is used if none is provided in search."""
+ client = Client(api_key="test_key", timeout=10)
+
+ def mock_request(method, url, params, headers, timeout, **kwargs):
+ assert timeout == 10
+ mock_response = requests.Response()
+ mock_response.status_code = 200
+ mock_response._content = b'{"search_metadata": {"id": "123"}}'
+ return mock_response
+
+ monkeypatch.setattr(client.session, "request", mock_request)
+
+ client.search(q="coffee")