Repository: CS-SI/eodag
Pull request #841 (2023-09-21 15:49:24)

feat: per provider search timetout
- Adds a default `timeout` (60s for `creodias`, `onda`, `wekeo`) in provider search configuration for search requests
- Adds some missing requests timeouts and user-agent
- Moves `HTTP_REQ_TIMEOUT` and `DEFAULT_STREAM_REQUESTS_TIMEOUT` to `eodag.utils`
- Less verbosity in `INFO` level for `DataRequestSearch` logging


github-actions[bot]: ## Test Results
0 files   -        4  0 suites   - 4   0s [:stopwatch:](https://github.com/EnricoMi/publish-unit-test-result-action/blob/v2.9.0/README.md#the-symbols "duration of all tests") - 8m 13s
0 tests  -    431  0 [:heavy_check_mark:](https://github.com/EnricoMi/publish-unit-test-result-action/blob/v2.9.0/README.md#the-symbols "passed tests")  -    428  0 [:zzz:](https://github.com/EnricoMi/publish-unit-test-result-action/blob/v2.9.0/README.md#the-symbols "skipped / disabled tests")  -   3  0 [:x:](https://github.com/EnricoMi/publish-unit-test-result-action/blob/v2.9.0/README.md#the-symbols "failed tests") ±0 
0 runs   - 1 724  0 [:heavy_check_mark:](https://github.com/EnricoMi/publish-unit-test-result-action/blob/v2.9.0/README.md#the-symbols "passed tests")  - 1 648  0 [:zzz:](https://github.com/EnricoMi/publish-unit-test-result-action/blob/v2.9.0/README.md#the-symbols "skipped / disabled tests")  - 76  0 [:x:](https://github.com/EnricoMi/publish-unit-test-result-action/blob/v2.9.0/README.md#the-symbols "failed tests") ±0 

Results for commit f566afdc. ± Comparison against base commit cb0217b5.

[test-results]:data:application/gzip;base64,H4sIAP1mDGUC/1WMQQrDIBBFrxJm3YXYVEMvU3R0YGgSy6irkrs3pKHV3X/vw3sD8Rwz3Ad1GSBXLj8IVVzhtJ64HyW3+5ErYi+e/OoEOZ47EUWSnEbqmpvZ5r78rx3cxA5uW5iWhcsOQDdjHAXUappsdIiofVTG22uwIXiymsYxooHtA5NUmvj7AAAA

github-actions[bot]: <strong>Code Coverage (Ubuntu)</strong>

| File | Coverage |   |
| - | :-: | :-: |
| **All files** | `82%` | :white_check_mark: |

_Minimum allowed coverage is `70%`_

<p align="right">Generated by :monkey: cobertura-action against 4c41af2c6b616dc1513221aaa7ef58f6ae6fae48 </p>
github-actions[bot]: <strong>Code Coverage (Windows)</strong>

| File | Coverage |   |
| - | :-: | :-: |
| **All files** | `76%` | :white_check_mark: |

_Minimum allowed coverage is `70%`_

<p align="right">Generated by :monkey: cobertura-action against 4c41af2c6b616dc1513221aaa7ef58f6ae6fae48 </p>

Patch:
diff --git a/eodag/api/core.py b/eodag/api/core.py
index 4d0a0626..45c31d6e 100644
--- a/eodag/api/core.py
+++ b/eodag/api/core.py
@@ -50,6 +50,7 @@ from eodag.plugins.search.base import Search
 from eodag.plugins.search.build_search_result import BuildPostSearchResult
 from eodag.utils import (
     GENERIC_PRODUCT_TYPE,
+    HTTP_REQ_TIMEOUT,
     MockResponse,
     _deprecated,
     deepcopy,
@@ -67,7 +68,7 @@ from eodag.utils.exceptions import (
     RequestError,
     UnsupportedProvider,
 )
-from eodag.utils.stac_reader import HTTP_REQ_TIMEOUT, fetch_stac_items
+from eodag.utils.stac_reader import fetch_stac_items
 
 logger = logging.getLogger("eodag.core")
 
diff --git a/eodag/api/product/_product.py b/eodag/api/product/_product.py
index 06958f04..facba318 100644
--- a/eodag/api/product/_product.py
+++ b/eodag/api/product/_product.py
@@ -28,12 +28,13 @@ from shapely.errors import ShapelyError
 
 from eodag.api.product.drivers import DRIVERS, NoDriver
 from eodag.api.product.metadata_mapping import NOT_AVAILABLE, NOT_MAPPED
-from eodag.plugins.download.base import (
-    DEFAULT_DOWNLOAD_TIMEOUT,
-    DEFAULT_DOWNLOAD_WAIT,
+from eodag.plugins.download.base import DEFAULT_DOWNLOAD_TIMEOUT, DEFAULT_DOWNLOAD_WAIT
+from eodag.utils import (
     DEFAULT_STREAM_REQUESTS_TIMEOUT,
+    USER_AGENT,
+    ProgressCallback,
+    get_geometry_from_various,
 )
-from eodag.utils import USER_AGENT, ProgressCallback, get_geometry_from_various
 from eodag.utils.exceptions import DownloadError, MisconfiguredError
 
 try:
diff --git a/eodag/config.py b/eodag/config.py
index 0e5a5eb1..faabf172 100644
--- a/eodag/config.py
+++ b/eodag/config.py
@@ -27,6 +27,7 @@ import yaml.parser
 from pkg_resources import resource_filename
 
 from eodag.utils import (
+    HTTP_REQ_TIMEOUT,
     USER_AGENT,
     cached_yaml_load,
     cached_yaml_load_all,
@@ -39,7 +40,6 @@ from eodag.utils import (
     uri_to_path,
 )
 from eodag.utils.exceptions import ValidationError
-from eodag.utils.stac_reader import HTTP_REQ_TIMEOUT
 
 logger = logging.getLogger("eodag.config")
 
diff --git a/eodag/plugins/authentication/keycloak.py b/eodag/plugins/authentication/keycloak.py
index 9c8f9934..bf6bd90b 100644
--- a/eodag/plugins/authentication/keycloak.py
+++ b/eodag/plugins/authentication/keycloak.py
@@ -21,9 +21,8 @@ import requests
 
 from eodag.plugins.authentication import Authentication
 from eodag.plugins.authentication.openid_connect import CodeAuthorizedAuth
-from eodag.utils import USER_AGENT
+from eodag.utils import HTTP_REQ_TIMEOUT, USER_AGENT
 from eodag.utils.exceptions import AuthenticationError, MisconfiguredError
-from eodag.utils.stac_reader import HTTP_REQ_TIMEOUT
 
 logger = logging.getLogger("eodag.plugins.auth.keycloak")
 
diff --git a/eodag/plugins/authentication/openid_connect.py b/eodag/plugins/authentication/openid_connect.py
index 85338d17..0d985c40 100644
--- a/eodag/plugins/authentication/openid_connect.py
+++ b/eodag/plugins/authentication/openid_connect.py
@@ -25,9 +25,8 @@ from lxml import etree
 from requests.auth import AuthBase
 
 from eodag.plugins.authentication import Authentication
-from eodag.utils import USER_AGENT, parse_qs, repeatfunc, urlparse
+from eodag.utils import HTTP_REQ_TIMEOUT, USER_AGENT, parse_qs, repeatfunc, urlparse
 from eodag.utils.exceptions import AuthenticationError, MisconfiguredError
-from eodag.utils.stac_reader import HTTP_REQ_TIMEOUT
 
 
 class OIDCAuthorizationCodeFlowAuth(Authentication):
diff --git a/eodag/plugins/authentication/qsauth.py b/eodag/plugins/authentication/qsauth.py
index e8daa018..8fa17d8b 100644
--- a/eodag/plugins/authentication/qsauth.py
+++ b/eodag/plugins/authentication/qsauth.py
@@ -22,9 +22,8 @@ import requests.auth
 from requests.exceptions import RequestException
 
 from eodag.plugins.authentication import Authentication
-from eodag.utils import USER_AGENT
+from eodag.utils import HTTP_REQ_TIMEOUT, USER_AGENT
 from eodag.utils.exceptions import AuthenticationError
-from eodag.utils.stac_reader import HTTP_REQ_TIMEOUT
 
 
 class HttpQueryStringAuth(Authentication):
diff --git a/eodag/plugins/authentication/sas_auth.py b/eodag/plugins/authentication/sas_auth.py
index f7e5a82f..49ded26b 100644
--- a/eodag/plugins/authentication/sas_auth.py
+++ b/eodag/plugins/authentication/sas_auth.py
@@ -22,9 +22,8 @@ import requests
 from requests.auth import AuthBase
 
 from eodag.plugins.authentication.base import Authentication
-from eodag.utils import USER_AGENT, deepcopy, format_dict_items
+from eodag.utils import HTTP_REQ_TIMEOUT, USER_AGENT, deepcopy, format_dict_items
 from eodag.utils.exceptions import AuthenticationError
-from eodag.utils.stac_reader import HTTP_REQ_TIMEOUT
 
 logger = logging.getLogger("eodag.plugins.auth.sas_auth")
 
diff --git a/eodag/plugins/authentication/token.py b/eodag/plugins/authentication/token.py
index 377268c7..11e9e2aa 100644
--- a/eodag/plugins/authentication/token.py
+++ b/eodag/plugins/authentication/token.py
@@ -23,9 +23,8 @@ from requests.adapters import HTTPAdapter
 from urllib3 import Retry
 
 from eodag.plugins.authentication.base import Authentication
-from eodag.utils import USER_AGENT, RequestsTokenAuth
+from eodag.utils import HTTP_REQ_TIMEOUT, USER_AGENT, RequestsTokenAuth
 from eodag.utils.exceptions import AuthenticationError, MisconfiguredError
-from eodag.utils.stac_reader import HTTP_REQ_TIMEOUT
 
 logger = logging.getLogger("eodag.authentication.token")
 
diff --git a/eodag/plugins/download/aws.py b/eodag/plugins/download/aws.py
index 4f53085f..0543c378 100644
--- a/eodag/plugins/download/aws.py
+++ b/eodag/plugins/download/aws.py
@@ -34,6 +34,7 @@ from eodag.api.product.metadata_mapping import (
 )
 from eodag.plugins.download.base import Download
 from eodag.utils import (
+    HTTP_REQ_TIMEOUT,
     USER_AGENT,
     ProgressCallback,
     flatten_top_directories,
@@ -42,7 +43,6 @@ from eodag.utils import (
     rename_subfolder,
 )
 from eodag.utils.exceptions import AuthenticationError, DownloadError, NotAvailableError
-from eodag.utils.stac_reader import HTTP_REQ_TIMEOUT
 
 logger = logging.getLogger("eodag.plugins.download.aws")
 
diff --git a/eodag/plugins/download/base.py b/eodag/plugins/download/base.py
index 1e040cef..cffa244b 100644
--- a/eodag/plugins/download/base.py
+++ b/eodag/plugins/download/base.py
@@ -40,7 +40,6 @@ logger = logging.getLogger("eodag.plugins.download.base")
 # default wait times in minutes
 DEFAULT_DOWNLOAD_WAIT = 2  # in minutes
 DEFAULT_DOWNLOAD_TIMEOUT = 20  # in minutes
-DEFAULT_STREAM_REQUESTS_TIMEOUT = 60  # in seconds
 
 
 class Download(PluginTopic):
diff --git a/eodag/plugins/download/http.py b/eodag/plugins/download/http.py
index 7ff41270..3d584dad 100644
--- a/eodag/plugins/download/http.py
+++ b/eodag/plugins/download/http.py
@@ -41,10 +41,11 @@ from eodag.api.product.metadata_mapping import (
 from eodag.plugins.download.base import (
     DEFAULT_DOWNLOAD_TIMEOUT,
     DEFAULT_DOWNLOAD_WAIT,
-    DEFAULT_STREAM_REQUESTS_TIMEOUT,
     Download,
 )
 from eodag.utils import (
+    DEFAULT_STREAM_REQUESTS_TIMEOUT,
+    HTTP_REQ_TIMEOUT,
     USER_AGENT,
     ProgressCallback,
     flatten_top_directories,
@@ -59,7 +60,6 @@ from eodag.utils.exceptions import (
     MisconfiguredError,
     NotAvailableError,
 )
-from eodag.utils.stac_reader import HTTP_REQ_TIMEOUT
 
 logger = logging.getLogger("eodag.plugins.download.http")
 
@@ -146,6 +146,7 @@ class HTTPDownload(Download):
             method=order_method,
             url=order_url,
             auth=auth,
+            timeout=HTTP_REQ_TIMEOUT,
             headers=dict(getattr(self.config, "order_headers", {}), **USER_AGENT),
             **order_kwargs,
         ) as response:
@@ -226,6 +227,7 @@ class HTTPDownload(Download):
             method=status_method,
             url=status_url,
             auth=auth,
+            timeout=HTTP_REQ_TIMEOUT,
             headers=dict(
                 getattr(self.config, "order_status_headers", {}), **USER_AGENT
             ),
@@ -278,7 +280,11 @@ class HTTPDownload(Download):
                         f"Search for new location: {product.properties['searchLink']}"
                     )
                     # search again
-                    response = requests.get(product.properties["searchLink"])
+                    response = requests.get(
+                        product.properties["searchLink"],
+                        timeout=HTTP_REQ_TIMEOUT,
+                        headers=USER_AGENT,
+                    )
                     response.raise_for_status()
                     if (
                         self.config.order_status_on_success.get("result_type", "json")
diff --git a/eodag/plugins/download/s3rest.py b/eodag/plugins/download/s3rest.py
index 2602df4c..87b30dd2 100644
--- a/eodag/plugins/download/s3rest.py
+++ b/eodag/plugins/download/s3rest.py
@@ -30,11 +30,12 @@ from eodag.api.product.metadata_mapping import OFFLINE_STATUS, ONLINE_STATUS
 from eodag.plugins.download.base import (
     DEFAULT_DOWNLOAD_TIMEOUT,
     DEFAULT_DOWNLOAD_WAIT,
-    DEFAULT_STREAM_REQUESTS_TIMEOUT,
     Download,
 )
 from eodag.plugins.download.http import HTTPDownload
 from eodag.utils import (
+    DEFAULT_STREAM_REQUESTS_TIMEOUT,
+    HTTP_REQ_TIMEOUT,
     USER_AGENT,
     ProgressCallback,
     get_bucket_name_and_prefix,
@@ -48,7 +49,6 @@ from eodag.utils.exceptions import (
     NotAvailableError,
     RequestError,
 )
-from eodag.utils.stac_reader import HTTP_REQ_TIMEOUT
 
 logger = logging.getLogger("eodag.plugins.download.s3rest")
 
diff --git a/eodag/plugins/search/data_request_search.py b/eodag/plugins/search/data_request_search.py
index 99523aab..6065c258 100644
--- a/eodag/plugins/search/data_request_search.py
+++ b/eodag/plugins/search/data_request_search.py
@@ -1,6 +1,6 @@
 import logging
 import time
-from datetime import datetime
+from datetime import datetime, timedelta
 
 import requests
 
@@ -15,11 +15,13 @@ from eodag.plugins.search.base import Search
 from eodag.rest.stac import DEFAULT_MISSION_START_DATE
 from eodag.utils import (
     GENERIC_PRODUCT_TYPE,
+    HTTP_REQ_TIMEOUT,
+    USER_AGENT,
     deepcopy,
     format_dict_items,
     string_to_jsonpath,
 )
-from eodag.utils.exceptions import RequestError
+from eodag.utils.exceptions import NotAvailableError, RequestError
 
 logger = logging.getLogger("eodag.search.data_request_search")
 
@@ -195,9 +197,24 @@ class DataRequestSearch(Search):
             data_request_id = self.data_request_id
             request_finished = True
 
+        # loop to check search job status
+        search_timeout = int(getattr(self.config, "timeout", HTTP_REQ_TIMEOUT))
+        logger.info(
+            f"checking status of request job {data_request_id} (timeout={search_timeout}s)"
+        )
+        check_beginning = datetime.now()
         while not request_finished:
             request_finished = self._check_request_status(data_request_id)
-            time.sleep(1)
+            if not request_finished and datetime.now() >= check_beginning + timedelta(
+                seconds=search_timeout
+            ):
+                self._cancel_request(data_request_id)
+                raise NotAvailableError(
+                    f"Timeout reached when checking search job status for {self.provider}"
+                )
+            elif not request_finished:
+                time.sleep(1)
+
         logger.info("search job for product_type %s finished", provider_product_type)
         result = self._get_result_data(
             data_request_id,
@@ -225,7 +242,7 @@ class DataRequestSearch(Search):
         )
 
     def _create_data_request(self, product_type, eodag_product_type, **kwargs):
-        headers = getattr(self.auth, "headers", "")
+        headers = getattr(self.auth, "headers", USER_AGENT)
         try:
             url = self.config.data_request_url
             request_body = format_query_params(
@@ -234,7 +251,9 @@ class DataRequestSearch(Search):
             logger.debug(
                 f"Sending search job request to {url} with {str(request_body)}"
             )
-            request_job = requests.post(url, json=request_body, headers=headers)
+            request_job = requests.post(
+                url, json=request_body, headers=headers, timeout=HTTP_REQ_TIMEOUT
+            )
             request_job.raise_for_status()
         except requests.RequestException as e:
             raise RequestError(
@@ -244,11 +263,24 @@ class DataRequestSearch(Search):
             logger.info("search job for product_type %s created", product_type)
             return request_job.json()["jobId"]
 
+    def _cancel_request(self, data_request_id):
+        logger.info("deleting request job %s", data_request_id)
+        delete_url = f"{self.config.data_request_url}/{data_request_id}"
+        try:
+            delete_resp = requests.delete(
+                delete_url, headers=self.auth.headers, timeout=HTTP_REQ_TIMEOUT
+            )
+            delete_resp.raise_for_status()
+        except requests.RequestException as e:
+            raise RequestError(f"_cancel_request failed: {str(e)}")
+
     def _check_request_status(self, data_request_id):
-        logger.info("checking status of request job %s", data_request_id)
+        logger.debug("checking status of request job %s", data_request_id)
         status_url = self.config.status_url + data_request_id
         try:
-            status_resp = requests.get(status_url, headers=self.auth.headers)
+            status_resp = requests.get(
+                status_url, headers=self.auth.headers, timeout=HTTP_REQ_TIMEOUT
+            )
             status_resp.raise_for_status()
         except requests.RequestException as e:
             raise RequestError(f"_check_request_status failed: {str(e)}")
@@ -273,7 +305,9 @@ class DataRequestSearch(Search):
             jobId=data_request_id, items_per_page=items_per_page, page=page
         )
         try:
-            return requests.get(url, headers=self.auth.headers).json()
+            return requests.get(
+                url, headers=self.auth.headers, timeout=HTTP_REQ_TIMEOUT
+            ).json()
         except requests.RequestException:
             logger.error(f"Result could not be retrieved for {url}")
 
diff --git a/eodag/plugins/search/qssearch.py b/eodag/plugins/search/qssearch.py
index 2fec1270..d8400ca4 100644
--- a/eodag/plugins/search/qssearch.py
+++ b/eodag/plugins/search/qssearch.py
@@ -37,6 +37,7 @@ from eodag.api.product.metadata_mapping import (
 from eodag.plugins.search.base import Search
 from eodag.utils import (
     GENERIC_PRODUCT_TYPE,
+    HTTP_REQ_TIMEOUT,
     USER_AGENT,
     _deprecated,
     deepcopy,
@@ -48,7 +49,6 @@ from eodag.utils import (
     urlencode,
 )
 from eodag.utils.exceptions import AuthenticationError, MisconfiguredError, RequestError
-from eodag.utils.stac_reader import HTTP_REQ_TIMEOUT
 
 logger = logging.getLogger("eodag.plugins.search.qssearch")
 
@@ -801,6 +801,7 @@ class QueryStringSearch(Search):
 
     def _request(self, url, info_message=None, exception_message=None):
         try:
+            timeout = getattr(self.config, "timeout", HTTP_REQ_TIMEOUT)
             # auth if needed
             kwargs = {}
             if (
@@ -828,14 +829,7 @@ class QueryStringSearch(Search):
                 if info_message:
                     logger.info(info_message.replace(url, prep.url))
                 urllib_req = Request(prep.url, headers=USER_AGENT)
-                urllib_response = urlopen(urllib_req)
-                # py2 compatibility : prevent AttributeError: addinfourl instance has no attribute 'reason'
-                if not hasattr(urllib_response, "reason"):
-                    urllib_response.reason = ""
-                if not hasattr(urllib_response, "status") and hasattr(
-                    urllib_response, "code"
-                ):
-                    urllib_response.status = urllib_response.code
+                urllib_response = urlopen(urllib_req, timeout=timeout)
                 # build Response
                 adapter = requests.adapters.HTTPAdapter()
                 response = adapter.build_response(prep, urllib_response)
@@ -843,7 +837,7 @@ class QueryStringSearch(Search):
                 if info_message:
                     logger.info(info_message)
                 response = requests.get(
-                    url, timeout=HTTP_REQ_TIMEOUT, headers=USER_AGENT, **kwargs
+                    url, timeout=timeout, headers=USER_AGENT, **kwargs
                 )
                 response.raise_for_status()
         except (requests.RequestException, URLError) as err:
@@ -1153,7 +1147,7 @@ class PostJsonSearch(QueryStringSearch):
                 url,
                 json=self.query_params,
                 headers=USER_AGENT,
-                timeout=HTTP_REQ_TIMEOUT,
+                timeout=getattr(self.config, "timeout", HTTP_REQ_TIMEOUT),
                 **kwargs,
             )
             response.raise_for_status()
diff --git a/eodag/plugins/search/static_stac_search.py b/eodag/plugins/search/static_stac_search.py
index 15ef0d76..d79cd3a8 100644
--- a/eodag/plugins/search/static_stac_search.py
+++ b/eodag/plugins/search/static_stac_search.py
@@ -25,8 +25,8 @@ from eodag.plugins.crunch.filter_date import FilterDate
 from eodag.plugins.crunch.filter_overlap import FilterOverlap
 from eodag.plugins.crunch.filter_property import FilterProperty
 from eodag.plugins.search.qssearch import StacSearch
-from eodag.utils import MockResponse
-from eodag.utils.stac_reader import HTTP_REQ_TIMEOUT, fetch_stac_items
+from eodag.utils import HTTP_REQ_TIMEOUT, MockResponse
+from eodag.utils.stac_reader import fetch_stac_items
 
 logger = logging.getLogger("eodag.plugins.search.static_stac_search")
 
diff --git a/eodag/resources/providers.yml b/eodag/resources/providers.yml
index 068a77bf..110cc24d 100644
--- a/eodag/resources/providers.yml
+++ b/eodag/resources/providers.yml
@@ -880,7 +880,8 @@
   search: !plugin
     type: ODataV4Search
     api_endpoint: 'https://datahub.creodias.eu/odata/v1/Products'
-    need_auth: false
+    timeout: 60
+    need_auth:
     dont_quote:
       - '['
       - ']'
@@ -1571,6 +1572,7 @@
   search: !plugin
     type: ODataV4Search
     api_endpoint: 'https://catalogue.onda-dias.eu/dias-catalogue/Products'
+    timeout: 60
     need_auth: false
     dont_quote:
       - '['
@@ -3745,6 +3747,7 @@
     metadata_url: "https://wekeo-broker.apps.mercator.dpi.wekeo.eu/databroker/querymetadata/"
     status_url: "https://wekeo-broker.apps.mercator.dpi.wekeo.eu/databroker/datarequest/status/"
     result_url: "https://wekeo-broker.apps.mercator.dpi.wekeo.eu/databroker/datarequest/jobs/{jobId}/result?size={items_per_page}&page={page}"
+    timeout: 60
     need_auth: true
     auth_error_code: 401
     results_entry: content
diff --git a/eodag/utils/__init__.py b/eodag/utils/__init__.py
index fd1ec1ab..d914aa28 100644
--- a/eodag/utils/__init__.py
+++ b/eodag/utils/__init__.py
@@ -80,15 +80,18 @@ except ImportError:  # pragma: no cover
     # for python < 3.8
     from importlib_metadata import metadata  # type: ignore
 
-DEFAULT_PROJ = "EPSG:4326"
-
 logger = py_logging.getLogger("eodag.utils")
 
+DEFAULT_PROJ = "EPSG:4326"
+
 GENERIC_PRODUCT_TYPE = "GENERIC_PRODUCT_TYPE"
 
 eodag_version = metadata("eodag")["Version"]
 USER_AGENT = {"User-Agent": f"eodag/{eodag_version}"}
 
+HTTP_REQ_TIMEOUT = 5  # in seconds
+DEFAULT_STREAM_REQUESTS_TIMEOUT = 60  # in seconds
+
 JSONPATH_MATCH = re.compile(r"^[\{\(]*\$(\..*)*$")
 WORKABLE_JSONPATH_MATCH = re.compile(r"^\$(\.[a-zA-Z0-9-_:\.\[\]\"\(\)=\?\*]+)*$")
 ARRAY_FIELD_MATCH = re.compile(r"^[a-zA-Z0-9-_:]+(\[[0-9\*]+\])+$")
diff --git a/eodag/utils/stac_reader.py b/eodag/utils/stac_reader.py
index 75fd8c5b..694f5ea7 100644
--- a/eodag/utils/stac_reader.py
+++ b/eodag/utils/stac_reader.py
@@ -25,12 +25,11 @@ import concurrent.futures
 import orjson
 import pystac
 
+from eodag.utils import HTTP_REQ_TIMEOUT
 from eodag.utils.exceptions import STACOpenerError
 
 logger = logging.getLogger("eodag.utils.stac_reader")
 
-HTTP_REQ_TIMEOUT = 5
-
 
 class _TextOpener:
     """Exhaust read methods for pystac.StacIO in the order defined
diff --git a/utils/params_mapping_to_csv.py b/utils/params_mapping_to_csv.py
index 70b31fa9..437fe40e 100644
--- a/utils/params_mapping_to_csv.py
+++ b/utils/params_mapping_to_csv.py
@@ -25,7 +25,7 @@ from lxml import html
 
 from eodag.api.core import EODataAccessGateway
 from eodag.config import load_stac_provider_config
-from eodag.utils.stac_reader import HTTP_REQ_TIMEOUT
+from eodag.utils import HTTP_REQ_TIMEOUT
 
 logger = logging.getLogger(__name__)
 logger.addHandler(logging.StreamHandler())


Test patch:
diff --git a/tests/context.py b/tests/context.py
index e0317006..7db6b3d8 100644
--- a/tests/context.py
+++ b/tests/context.py
@@ -58,7 +58,6 @@ from eodag.plugins.crunch.filter_overlap import FilterOverlap
 from eodag.plugins.download.aws import AwsDownload
 from eodag.plugins.download.base import (
     Download,
-    DEFAULT_STREAM_REQUESTS_TIMEOUT,
     DEFAULT_DOWNLOAD_WAIT,
 )
 from eodag.plugins.download.http import HTTPDownload
@@ -66,6 +65,8 @@ from eodag.plugins.manager import PluginManager
 from eodag.plugins.search.base import Search
 from eodag.rest.stac import DEFAULT_MISSION_START_DATE
 from eodag.utils import (
+    DEFAULT_STREAM_REQUESTS_TIMEOUT,
+    HTTP_REQ_TIMEOUT,
     USER_AGENT,
     get_bucket_name_and_prefix,
     get_geometry_from_various,
@@ -99,6 +100,6 @@ from eodag.utils.exceptions import (
     ValidationError,
     STACOpenerError,
 )
-from eodag.utils.stac_reader import fetch_stac_items, HTTP_REQ_TIMEOUT, _TextOpener
+from eodag.utils.stac_reader import fetch_stac_items, _TextOpener
 from tests import TEST_RESOURCES_PATH
 from usgs.api import USGSAuthExpiredError, USGSError
diff --git a/tests/integration/test_download_auth.py b/tests/integration/test_download_auth.py
index 3261806a..f2bc069e 100644
--- a/tests/integration/test_download_auth.py
+++ b/tests/integration/test_download_auth.py
@@ -18,6 +18,8 @@
 
 import os
 import unittest
+from tempfile import TemporaryDirectory
+from unittest import mock
 
 from pkg_resources import resource_filename
 
@@ -33,11 +35,31 @@ class TestEODagDownloadCredentialsNotSet(unittest.TestCase):
     # Given a GeoJSON Search Result it can however download the products.
 
     @classmethod
-    def setUpClass(self):
+    def setUpClass(cls):
+        super(TestEODagDownloadCredentialsNotSet, cls).setUpClass()
+        # Mock home and eodag conf directory to tmp dir
+        cls.tmp_home_dir = TemporaryDirectory()
+        expanduser_mock_side_effect = (
+            lambda *x: x[0]
+            .replace("~user", cls.tmp_home_dir.name)
+            .replace("~", cls.tmp_home_dir.name)
+        )
+        cls.expanduser_mock = mock.patch(
+            "os.path.expanduser", autospec=True, side_effect=expanduser_mock_side_effect
+        )
+        cls.expanduser_mock.start()
+
         default_conf_file = resource_filename(
             "eodag", os.path.join("resources", "user_conf_template.yml")
         )
-        self.eodag = EODataAccessGateway(user_conf_file_path=default_conf_file)
+        cls.eodag = EODataAccessGateway(user_conf_file_path=default_conf_file)
+
+    @classmethod
+    def tearDownClass(cls):
+        super(TestEODagDownloadCredentialsNotSet, cls).tearDownClass()
+        # stop Mock and remove tmp config dir
+        cls.expanduser_mock.stop()
+        cls.tmp_home_dir.cleanup()
 
     def test_eodag_download_missing_credentials_theia(self):
         search_resuls = os.path.join(
diff --git a/tests/units/test_download_plugins.py b/tests/units/test_download_plugins.py
index 658c80bd..7e2fb468 100644
--- a/tests/units/test_download_plugins.py
+++ b/tests/units/test_download_plugins.py
@@ -468,6 +468,7 @@ class TestDownloadPluginHttp(BaseDownloadPluginTest):
             url=self.product.properties["orderLink"],
             auth=auth,
             headers=USER_AGENT,
+            timeout=HTTP_REQ_TIMEOUT,
         )
 
     @mock.patch("eodag.plugins.download.http.requests.request", autospec=True)
@@ -489,6 +490,7 @@ class TestDownloadPluginHttp(BaseDownloadPluginTest):
             url=self.product.properties["orderLink"],
             auth=auth,
             headers=USER_AGENT,
+            timeout=HTTP_REQ_TIMEOUT,
         )
         # orderLink with query query args
         mock_request.reset_mock()
@@ -499,6 +501,7 @@ class TestDownloadPluginHttp(BaseDownloadPluginTest):
             url="http://somewhere/order",
             auth=auth,
             headers=USER_AGENT,
+            timeout=HTTP_REQ_TIMEOUT,
             json={"foo": ["bar"]},
         )
 
diff --git a/tests/units/test_search_plugins.py b/tests/units/test_search_plugins.py
index f293fa1b..8d018123 100644
--- a/tests/units/test_search_plugins.py
+++ b/tests/units/test_search_plugins.py
@@ -1231,6 +1231,7 @@ class TestSearchPluginDataRequestSearch(BaseSearchPluginTest):
             self.search_plugin.config.data_request_url,
             json={"datasetId": "EO:DEM:DAT:COP-DEM_GLO-30-DGED__2022_1"},
             headers=getattr(self.search_plugin.auth, "headers", ""),
+            timeout=HTTP_REQ_TIMEOUT,
         )
         keywords = {
             "format": "GeoTiff100mt",
@@ -1252,6 +1253,7 @@ class TestSearchPluginDataRequestSearch(BaseSearchPluginTest):
                 ],
             },
             headers=getattr(self.search_plugin.auth, "headers", ""),
+            timeout=HTTP_REQ_TIMEOUT,
         )
 
     @mock.patch("eodag.plugins.search.data_request_search.requests.get", autospec=True)
@@ -1261,6 +1263,7 @@ class TestSearchPluginDataRequestSearch(BaseSearchPluginTest):
         mock_requests_get.assert_called_with(
             self.search_plugin.config.status_url + "123",
             headers=getattr(self.search_plugin.auth, "headers", ""),
+            timeout=HTTP_REQ_TIMEOUT,
         )
         assert successful
         mock_requests_get.return_value = MockResponse(
@@ -1277,6 +1280,7 @@ class TestSearchPluginDataRequestSearch(BaseSearchPluginTest):
                 jobId="123", items_per_page=5, page=0
             ),
             headers=getattr(self.search_plugin.auth, "headers", ""),
+            timeout=HTTP_REQ_TIMEOUT,
         )
 
     def test_plugins_search_datareq_distinct_product_type_mtd_mapping(self):